/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.gwt.remoteaction.rebind;

import com.google.code.gwt.remoteaction.client.RemoteAction;
import com.google.code.gwt.remoteaction.server.Utils;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JGenericType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.name.Named;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

// TODO: add a check that async methods do not throw exceptions
// TODO: add a check that sync & async interfaces (and enclosing classes) are public
/**
 * A class for checking that a remote action service interface is valid.
 *
 * @author jgennis
 */
class RemoteActionServiceValidator {

  protected GeneratorContext context;

  protected TypeOracle typeOracle;

  protected JClassType serviceInterface;

  protected JClassType serviceAsyncInterface;

  public RemoteActionServiceValidator(GeneratorContext context, JClassType serviceInterface) {
    if (context == null)
      throw new IllegalArgumentException("The 'context' argument may not be null");
    if (serviceInterface == null)
      throw new IllegalArgumentException("The 'remoteActionServiceType' argument may not be null");

    this.context = context;
    this.typeOracle = context.getTypeOracle();
    this.serviceInterface = serviceInterface;
  }

  public void validate(TreeLogger logger) throws UnableToCompleteException {
    TreeLogger validationLogger = logger.branch(TreeLogger.Type.DEBUG, "Validating remote " +
        "action service '" + serviceInterface.getName() + "'");

    if (serviceInterface.isInterface() == null) {
      validationLogger.log(TreeLogger.Type.ERROR, "Remote action service '" +
          serviceInterface.getQualifiedSourceName() + "' must be an interface");
      throw new UnableToCompleteException();
    }

    serviceAsyncInterface = typeOracle.findType(serviceInterface.getQualifiedSourceName() +
        "Async");
    if (serviceAsyncInterface == null) {
      validationLogger.log(TreeLogger.Type.ERROR, "No async interface for remote action service " +
          "'" + serviceInterface.getQualifiedSourceName() + "' was found");
      throw new UnableToCompleteException();
    }
    if (serviceAsyncInterface.isInterface() == null) {
      validationLogger.log(TreeLogger.Type.ERROR, "Asynchronous remote action service '" +
          serviceAsyncInterface.getQualifiedSourceName() + "' must be an interface");
      throw new UnableToCompleteException();
    }

    validateRemoteActionService(validationLogger);
    validateRemoteActionServiceAsync(validationLogger);
  }

  protected void validateRemoteActionService(TreeLogger logger) throws UnableToCompleteException {
    TreeLogger interfaceLogger = logger.branch(TreeLogger.Type.DEBUG, "Validating interface '" +
        serviceInterface.getQualifiedSourceName() + "'");
    JMethod[] actionMethods = serviceInterface.getMethods();
    for (JMethod actionMethod : actionMethods) {
      TreeLogger methodLogger = interfaceLogger.branch(TreeLogger.Type.DEBUG, "Checking method '" +
          actionMethod.toString() + "'");
      RemoteAction remoteAction = actionMethod.getAnnotation(RemoteAction.class);
      if (remoteAction == null) {
        methodLogger.log(TreeLogger.Type.ERROR, "No @RemoteAction annotation found");
        throw new UnableToCompleteException();
      }
      Class<?> actionClass = remoteAction.value();
      if (actionClass == null) {
        methodLogger.log(TreeLogger.Type.ERROR, "No action class specified in the @RemoteAction " +
            "annotation");
        throw new UnableToCompleteException();
      }
      JClassType actionType = typeOracle.findType(remoteAction.value().getCanonicalName());
      if (actionType == null) {
        // Can this even get reached?
        methodLogger.log(TreeLogger.Type.ERROR, "Unknown action class '" +
            actionClass.getCanonicalName() + "'");
        throw new UnableToCompleteException();
      }
      JParameter[] params = actionMethod.getParameters();
      validateActionConstructors(methodLogger, params, actionType);
      validateActionProperties(methodLogger, params, actionType);
    }
  }

  protected void validateActionConstructors(TreeLogger logger, JParameter[] params,
      JClassType actionType) throws UnableToCompleteException {
    List<JType> paramTypes = new ArrayList<JType>();
    for (JParameter param : params)
      paramTypes.add(param.getType());
    JConstructor constructor = actionType.findConstructor(paramTypes.toArray(new JType[0]));
    if (constructor == null || !constructor.isPublic()) {
      logger.log(TreeLogger.Type.ERROR, "Action class '" + actionType.getQualifiedSourceName() +
          "' does not have a public constructor with matching parameter types");
      throw new UnableToCompleteException();
    }
    constructor = actionType.findConstructor(new JType[]{});
    if (constructor == null) {
      logger.log(TreeLogger.Type.ERROR, "Action class '" + actionType.getQualifiedSourceName() +
          "' must have a (possibly non-public) 0-argument constructor");
      throw new UnableToCompleteException();
    }
  }

  protected void validateActionProperties(TreeLogger logger, JParameter[] params,
      JClassType actionType) throws UnableToCompleteException {
    for (JParameter param : params) {
      JType paramType = param.getType();
      Named named = param.getAnnotation(Named.class);
      if (named == null) {
        int numProperties = countProperties(actionType, paramType);
        if (numProperties == 0) {
          logger.log(TreeLogger.Type.ERROR, "Action class '" +
              actionType.getQualifiedSourceName() + "' does not contain a public property of " +
              "type '" + paramType.getQualifiedSourceName() + "', which is needed for the '" +
              param.getName() + "' parameter");
          throw new UnableToCompleteException();
        } else if (numProperties > 1) {
          logger.log(TreeLogger.Type.ERROR, "Action class '" +
              actionType.getQualifiedSourceName() + "' contains multiple public properties of " +
              "type '" + paramType.getQualifiedSourceName() + "'.  Use the @Named annotation on " +
              "the '" + param.getName() + "' parameter to indicate which property should be used");
          throw new UnableToCompleteException();
        }
      } else {
        String propertyName = named.value();
        if (!hasProperty(actionType, paramType, propertyName)) {
          logger.log(TreeLogger.Type.ERROR, "Action class '" +
              actionType.getQualifiedSourceName() + "' does not contain a public property of " +
              "type '" + paramType.getQualifiedSourceName() + "' named '" + propertyName + "', " +
              "which is needed for the '" + param.getName() + "' parameter");
          throw new UnableToCompleteException();
        }
      }
    }
  }

  protected boolean hasProperty(JClassType classType, JType propertyType, String propertyName) {
    String getterName = Utils.makeGetterName(propertyName);
    JMethod propertyGetter = classType.findMethod(getterName, new JType[0]);
    if (propertyGetter != null && propertyGetter.isPublic() &&
        propertyType.equals(propertyGetter.getReturnType())) {
      return true;
    }
    JField propertyField = classType.findField(propertyName);
    if (propertyField != null && propertyField.isPublic() &&
        propertyType.equals(propertyField.getType())) {
      return true;
    }
    return false;
  }

  protected int countProperties(JClassType classType, JType propertyType) {
    Set<String> getters = new HashSet<String>();
    for (JMethod method : classType.getMethods()) {
      if (method.getName().matches("^get[A-Z].*") && method.getParameters().length == 0 &&
          method.isPublic() && propertyType.equals(method.getReturnType())) {
        getters.add(method.getName());
      }
    }
    int numFieldsFound = 0;
    for (JField field : classType.getFields()) {
      if (field.isPublic() && propertyType.equals(field.getType()) &&
          !getters.contains(Utils.makeGetterName(field.getName()))) {
        ++numFieldsFound;
      }
    }
    return numFieldsFound + getters.size();
  }

  protected void validateRemoteActionServiceAsync(TreeLogger logger)
      throws UnableToCompleteException {
    TreeLogger interfaceLogger = logger.branch(TreeLogger.Type.DEBUG, "Validating interface '" +
        serviceAsyncInterface.getQualifiedSourceName() + "'");
    JMethod[] asyncMethods = serviceAsyncInterface.getMethods();
    if (asyncMethods.length != serviceInterface.getMethods().length) {
      interfaceLogger.log(TreeLogger.Type.ERROR, "Method count does not match remote action " +
          "service interface '" + serviceInterface.getQualifiedSourceName() + "'");
      throw new UnableToCompleteException();
    }
    for (JMethod asyncMethod : asyncMethods) {
      TreeLogger methodLogger = interfaceLogger.branch(TreeLogger.Type.DEBUG, "Checking method '" +
          asyncMethod + "'");
      validateAsyncMethod(methodLogger, asyncMethod);
    }
  }

  protected void validateAsyncMethod(TreeLogger logger, JMethod method)
      throws UnableToCompleteException {
    // Check that the method returns 'void'
    if (!method.getReturnType().equals(JPrimitiveType.VOID)) {
      logger.log(TreeLogger.Type.ERROR, "Method must have a return type of 'void'");
      throw new UnableToCompleteException();
    }

    // Check that the parameter count is > 0
    JParameter[] params = method.getParameters();
    if (params.length == 0) {
      logger.log(TreeLogger.Type.ERROR, "Method takes no parameters.  Asynchronous action " +
          "methods must accept an AsyncCallback parameter.");
      throw new UnableToCompleteException();
    }

    // Check that there is a corresponding method in the synchronous interface
    ArrayList<JType> serviceMethodParamTypes = new ArrayList<JType>();
    for (int i = 0; i < params.length - 1; ++i) {
      serviceMethodParamTypes.add(params[i].getType());
    }
    JMethod serviceMethod = serviceInterface.findMethod(method.getName(),
        serviceMethodParamTypes.toArray(new JType[0]));
    if (serviceMethod == null) {
      logger.log(TreeLogger.Type.ERROR, "No matching method found in remote action service " +
          "interface '" + serviceInterface.getQualifiedSourceName() + "'");
      throw new UnableToCompleteException();
    }

    // Check that the last parameter is an AsyncCallback with the correct type
    // parameterization
    JType returnType = serviceMethod.getReturnType();
    JPrimitiveType returnPrimitiveType = returnType.isPrimitive();
    if (returnPrimitiveType != null) // Box the return type
      returnType = typeOracle.findType(returnPrimitiveType.getQualifiedBoxedSourceName());
    JGenericType asyncCallbackGeneric = typeOracle.findType(
        AsyncCallback.class.getCanonicalName()).isGenericType();
    JParameterizedType asyncCallbackType = typeOracle.getParameterizedType(asyncCallbackGeneric,
        new JClassType[]{returnType.isClassOrInterface()});
    if (!asyncCallbackType.equals(params[params.length - 1].getType())) {
      logger.log(TreeLogger.Type.ERROR, "The type of the last parameter must be '" +
          asyncCallbackType.getParameterizedQualifiedSourceName() + "'");
      throw new UnableToCompleteException();
    }
  }
}
